package network

import (
	"encoding/json"
	"fmt"
	"github.com/sirupsen/logrus"
	"github.com/vishvananda/netlink"
	"github.com/vishvananda/netns"
	"io/fs"
	"mydocker/common"
	"mydocker/container"
	"net"
	"os"
	"os/exec"
	"path"
	"path/filepath"
	"runtime"
	"strings"
	"text/tabwriter"
)

var drivers = map[string]Driver{}

// dump 将 network 信息存储到文件中
func (net *Network) dump(dumpPath string) error {
	if _, err := os.Stat(dumpPath); err != nil && os.IsNotExist(err) {
		err = os.MkdirAll(dumpPath, os.ModePerm)
		if err != nil {
			return err
		}
	}

	nwPath := path.Join(dumpPath, net.Name)
	nwFile, err := os.OpenFile(nwPath, os.O_TRUNC|os.O_WRONLY|os.O_CREATE, 0644)
	if err != nil {
		logrus.Errorf("error: %v\n", err)
		return err
	}
	defer nwFile.Close()

	nwJson, _ := json.Marshal(net)
	_, err = nwFile.Write(nwJson)
	if err != nil {
		logrus.Errorf("write network file, err: %v\n", err)
		return err
	}
	return nil
}

// remove 移除 network 的信息文件
func (net *Network) remove(dumpPath string) error {
	if _, err := os.Stat(path.Join(dumpPath, net.Name)); err != nil && os.IsNotExist(err) {
		return nil
	}
	return os.RemoveAll(path.Join(dumpPath, net.Name))
}

// load 从文件中加载网络配置，并将其反序列化为 Network 类型的对象。
func (net *Network) load(dumpPath string) error {
	// 打开配置文件
	netConfigFile, err := os.Open(dumpPath)
	if err != nil {
		return err
	}
	defer netConfigFile.Close()

	// 从配置文件中读取网络配置 json 字符串
	netJson := make([]byte, 2000)
	n, err := netConfigFile.Read(netJson)
	if err != nil {
		return err
	}

	err = json.Unmarshal(netJson[:n], net)
	if err != nil {
		logrus.Errorf("json unmarshal network info, err: %v\n", err)
		return err
	}
	return nil
}

// Init 初始化网络驱动
func Init() {
	// 加载网络驱动
	var bridgeDriver = BridgeNetworkDriver{}
	drivers[bridgeDriver.Name()] = &bridgeDriver

	// 文件不存在则创建
	if _, err := os.Stat(common.DefaultNetWorkPath); err != nil {
		if !os.IsNotExist(err) {
			logrus.Errorf("check %s is exists failed, detail: %v", common.DefaultNetWorkPath, err)
			return
		}
		if err = os.MkdirAll(common.DefaultNetWorkPath, common.Perm0644); err != nil {
			logrus.Errorf("create %s failed, detail: %v", common.DefaultNetWorkPath, err)
			return
		}
	}

}

// CreateNetwork 根据不同 driver 创建 Network
func CreateNetwork(driver, subnet, name string) error {
	// 将网段的字符串转换成 IP，IPNet 的对象，即类似于 127.0.0.0，127.0.0.0/24
	_, ipNet, err := net.ParseCIDR(subnet)
	if err != nil {
		logrus.Errorf("parse cidr, err: %v\n", err)
		return err
	}
	// 通过 IPAM 分配网关 IP，获取到网段中第一个 IP 作为网关的 IP
	ip, err := ipAllocator.Allocate(ipNet)
	if err != nil {
		logrus.Errorf("allocate ip, err: %v\n", err)
		return err
	}
	ipNet.IP = ip

	// 调用指定的网络驱动创建网络，这里的 drivers 字典是各个网络驱动的实例字典，
	// 通过调用网络驱动 Create 方法创建网络，后面会以 Bridge 驱动为例介绍它的实现
	nw, err := drivers[driver].Create(ipNet.String(), name)
	if err != nil {
		return err
	}

	// 将对象保存到文件中
	err = nw.dump(common.DefaultNetWorkPath)
	if err != nil {
		logrus.Errorf("dump network, err: %v\n", err)
		return err
	}
	return nil
}

// ListNetwork 打印出当前全部 Network 信息
func ListNetwork() {
	networks, err := loadNetwork()
	if err != nil {
		logrus.Errorf("load network from file failed,detail: %v\n", err)
		return
	}
	// 通过 tabwriter 库把信息打印到屏幕上
	w := tabwriter.NewWriter(os.Stdout, 12, 1, 3, ' ', 0)
	fmt.Fprint(w, "NAME\tIpRange\tDriver\n")
	for _, net := range networks {
		fmt.Fprintf(w, "%s\t%s\t%s\n", net.Name, net.IpRange.String(), net.Driver)
	}
	if err = w.Flush(); err != nil {
		logrus.Errorf("Flush err: %v\n", err)
		return
	}
}

// DeleteNetwork 根据名字删除 Network
func DeleteNetwork(networkName string) error {
	networks, err := loadNetwork()
	if err != nil {
		logrus.Errorf("load network from file failed,detail: %v\n", err)
		return err
	}
	// 网络不存在直接返回一个 error
	net, ok := networks[networkName]
	if !ok {
		return fmt.Errorf("no Such Network: %s", networkName)
	}
	// 调用 IPAM 的实例 ipAllocator 释放网络网关的 IP
	if err = ipAllocator.Release(net.IpRange, &net.IpRange.IP); err != nil {
		logrus.Errorf("release ip, err: %v\n", err)
	}
	// 调用网络驱动删除网络创建的设备和配置 后面会以 Bridge 驱动删除网络为例子介绍如何实现网络驱动删除网络
	if err = drivers[net.Driver].Delete(net); err != nil {
		logrus.Errorf("remove Network DriverError failed: %s", err)
		return err
	}
	// 最后从网络的配置目录中删除该网络对应的配置文件
	return net.remove(common.DefaultNetWorkPath)
}

// Connect 连接容器到之前创建的网络 mydocker run -net testnet -p 8080:80 xxxx
func Connect(networkName string, containerInfo *container.ContainerInfo) (net.IP, error) {
	// 读取存放 network 信息的所有文件，并将其存储在 networks 中
	networks, err := loadNetwork()
	if err != nil {
		logrus.Errorf("load network from file err: %v\n", err)
		return nil, err
	}
	// 从 networks 字典中取到容器连接的网络信息，networks 字典中保存了当前已经创建的网络
	network, ok := networks[networkName]
	if !ok {
		return nil, fmt.Errorf("network %s not found", networkName)
	}

	// 分配容器 IP 地址
	ip, err := ipAllocator.Allocate(network.IpRange)
	if err != nil {
		logrus.Errorf("allocate ip, err: %v\n", err)
		return ip, err
	}
	// 创建网络端点
	ep := &Endpoint{
		ID:          fmt.Sprintf("%s-%s", containerInfo.ID, networkName),
		IPAddress:   ip,
		Network:     network,
		PortMapping: containerInfo.PortMapping,
	}
	// 调用网络驱动挂载和配置网络端点
	if err = drivers[network.Driver].Connect(network.Name, ep); err != nil {
		return ip, err
	}
	// 给容器的 namespace 配置容器网络设备 IP 地址
	if err = configEndpointIpAddressAndRoute(ep, containerInfo); err != nil {
		return ip, err
	}
	// 配置端口映射信息，例如 mydocker run -p 8080:80
	return ip, addPortMapping(ep)
}

// Disconnect 将容器从指定网络中移除
func Disconnect(networkName string, info *container.ContainerInfo) error {
	networks, err := loadNetwork()
	if err != nil {
		logrus.Errorf("load network from file err: %v\n", err)
		return err
	}
	// 从 networks 字典中取到容器连接的网络信息，networks 字典中保存了当前已经创建的网络
	network, ok := networks[networkName]
	if !ok {
		return fmt.Errorf("network %s not found", networkName)
	}
	// veth 从 bridge 解绑并删除 veth-pair 设备对
	drivers[network.Driver].Disconnect(fmt.Sprintf("%s-%s", info.ID, networkName))

	// 清理端口映射添加的 iptables 规则
	ep := &Endpoint{
		ID:          fmt.Sprintf("%s-%s", info.ID, networkName),
		IPAddress:   net.ParseIP(info.IP),
		Network:     network,
		PortMapping: info.PortMapping,
	}
	return deletePortMapping(ep)
}

// loadNetwork 读取 defaultNetworkPath 目录下的 Network 信息存放到内存中，便于使用
func loadNetwork() (map[string]*Network, error) {
	networks := make(map[string]*Network)
	// 检查网络配置目录中的所有文件，并执行第二个参数中的函数指针去处理目录下的每一个文件
	err := filepath.Walk(common.DefaultNetWorkPath, func(netPath string, info fs.FileInfo, err error) error {
		// 如果是目录则跳过
		if info.IsDir() {
			return nil
		}
		// 加载文件名作为网络名
		_, netName := path.Split(netPath)
		net := &Network{
			Name: netName,
		}
		// 调用前面介绍的 Network.load 方法加载网络的配置信息
		if err = net.load(netPath); err != nil {
			logrus.Errorf("error load network, err: %v\n", err)
		}
		// 将网络的配置信息加入到 networks 字典中
		networks[net.Name] = net
		return nil
	})
	return networks, err
}

// configEndpointIpAddressAndRoute 配置容器网络端点的地址和路由
func configEndpointIpAddressAndRoute(ep *Endpoint, containerInfo *container.ContainerInfo) error {
	// 根据名字找到对应 Veth 设备
	peerLink, err := netlink.LinkByName(ep.Device.PeerName)
	if err != nil {
		logrus.Errorf("fail config endpoint: %v\n", err)
		return err
	}

	// 将容器的网络端点加入到容器的网络空间中
	// 并使这个函数下面的操作都在这个网络空间中进行
	// 执行完函数后，恢复为默认的网络空间
	defer enterContainerNetNS(&peerLink, containerInfo)()

	// 获取到容器的 网段及IP地址。用于配置容器内部接口地址
	// 比如容器 IP 是 192.168.1.2，而网络的网段是 192.168.1.0/24
	// 那么这里产出的 IP 字符串就是 192.168.1.2/24，用于容器内 Veth 端点配置
	interfaceIPNet := *ep.Network.IpRange
	interfaceIPNet.IP = ep.IPAddress
	// 设置容器内 Veth 端点的 IP
	if err = setInterfaceIP(ep.Device.PeerName, interfaceIPNet.String()); err != nil {
		logrus.Errorf("fail config endpoint: %v\n", err)
		return err
	}
	// 启动容器内的 Veth 端点
	if err = setInterfaceUP(ep.Device.PeerName); err != nil {
		logrus.Errorf("fail config endpoint: %v\n", err)
		return err
	}
	// Net Namespace 中默认本地地址 127.0.0.1 的网卡是关闭状态的
	// 启动它以保证容器访问自己的请求
	if err = setInterfaceUP("lo"); err != nil {
		logrus.Errorf("fail config endpoint: %v\n", err)
		return err
	}
	// 设置容器内的外部请求都通过容器内的 Veth 端点访问
	// 0.0.0.0/0 的网段，表示所有的 IP 地址段
	_, iPNet, _ := net.ParseCIDR("0.0.0.0/0")
	// 构建需要添加的路由数据，包括网络设备、网关 IP 及目的网段
	// 相当于 route add -net 0.0.0.0/0 gw (Bridge网桥地址) dev (容器内的veth端点设备)
	defaultRoute := &netlink.Route{
		LinkIndex: peerLink.Attrs().Index,
		Gw:        ep.Network.IpRange.IP,
		Dst:       iPNet, // 网络地址的范围
	}
	// 调用 netlink 的 RouterAdd，添加路由到容器的网络空间
	// RouteAdd 函数相当于 route add 命令
	if err = netlink.RouteAdd(defaultRoute); err != nil {
		logrus.Errorf("fail config endpoint: %v\n", err)
		return err
	}
	return nil
}

// enterContainerNets 将容器的网络端点加入到容器的网络空间中
// 并锁定当前程序所执行的线程，使当前线程进入到容器的网络空间
// 返回值是一个函数指针，执行这个返回函数才会退出容器的网络空间，回归到宿主机的网络空间
func enterContainerNetNS(enLink *netlink.Link, continerInfo *container.ContainerInfo) func() {
	// 找到容器的 Net namespace
	// /proc/[pid]/ns/net 打开这个文件的文件描述符就可以进来操控 Net Namespace
	// 而 ContainerInfo 中的 PID，即容器在宿主机上映射的进程 ID
	// 它对应的 /proc/[pid]/ns/net 就是容器内部的 Net Namespace
	f, err := os.OpenFile(fmt.Sprintf("/proc/%s/ns/net", continerInfo.Pid), os.O_RDONLY, 0)
	if err != nil {
		logrus.Errorf("error get container net namespace %v\n", err)
	}

	// Fd 返回与文件 f 对应的整数类型的 Unix 文件描述符
	// Unix 文件描述符（File Descriptor）是 Unix 和类 Unix 操作系统中用于表示打开文件或其他 I/O 资源的抽象指标。
	// 文件描述符是一个非负整数，用于在程序中唯一标识一个打开的文件或 I/O 通道。
	// 唯一性：每个打开的文件或设备都有一个唯一的文件描述符，即使是相同的文件，每次打开都会获得一个新的文件描述符。
	nsFD := f.Fd()
	// 锁定当前程序所执行的线程，如果不锁定操作系统线程的话
	// Go语言的 goroutine 可能会被调度到别的线程上去了
	// 就不能保证一直在所需要的网络空间中了
	// 所以要先调用 runtime.LockOSThread() 锁定当前程序执行的线程
	runtime.LockOSThread() // 将调用的 go 程绑定到他当前所在的操作系统线程。
	// 除非调用的 go 程退出或调用 UnlockOSThread，否则它将总是在该线程中执行，而其他 go 程则不能进入该线程

	// 修改网络端点 Veth 的另外一端，将其移动到容器的 Net Namespace 中
	if err = netlink.LinkSetNsFd(*enLink, int(nsFD)); err != nil {
		logrus.Errorf("error set container net namespace %v\n", err)
	}

	// 获取当前的网络 Namespace，以便于后续退出容器时，恢复到这个网络 Namespace
	orgins, err := netns.Get()
	if err != nil {
		logrus.Errorf("error get net namespace %v\n", err)
	}

	// 调用 netns.Set 方法，将当前进程加入容器的 Net Namespace
	if err = netns.Set(netns.NsHandle(nsFD)); err != nil {
		logrus.Errorf("error set net namespace %v\n", err)
	}
	// 返回之前 Net Namespace 的函数
	// 在容器的网络空间中执行完容器配置之后调用此函数就可以将程序恢复到原生的 Net Namespace
	return func() {
		// 恢复到上面获取到的之前的 Net Namespace
		netns.Set(orgins)
		orgins.Close()
		// 取消对当前程序的线程锁定
		runtime.UnlockOSThread()
		f.Close()
	}
}

func addPortMapping(ep *Endpoint) error {
	return configPortMapping(ep, false)
}

func deletePortMapping(ep *Endpoint) error {
	return configPortMapping(ep, true)
}

// configPortMapping 配置端口映射
func configPortMapping(ep *Endpoint, isDelete bool) error {
	action := "-A"
	if isDelete {
		action = "-D"
	}

	var err error
	// 遍历容器端口映射列表
	for _, pm := range ep.PortMapping {
		// 分割成宿主机的端口和容器的端口
		portMapping := strings.Split(pm, ":")
		if len(portMapping) != 2 {
			logrus.Errorf("port mapping format error, %v", pm)
			continue
		}
		// 由于 iptables 没有 Go语言版本的实现，所以采用 exec.Command 的方式直接调用命令配置
		// iptables 的 PREROUTING 添加 DNAT 规则
		// 将宿主机的端口请求发送到容器的地址和端口上
		// iptables -t nat -A PREROUTING ! -i testbridge -p tcp -m tcp --dport 8080 -j DNAT --to-destination 10.0.0.4:80
		iptablesCmd := fmt.Sprintf("-t nat %s PREROUTING ! -i %s -p tcp -m tcp --dport %s -j DNAT --to-destination %s:%s", action, ep.Network, portMapping[0], ep.IPAddress.String(), portMapping[1])
		cmd := exec.Command("iptables", strings.Split(iptablesCmd, " ")...)
		logrus.Infoln("配置端口映射 DNAT cmd：", cmd.String())
		// 执行 iptables 命令，添加端口映射转发规则
		output, err := cmd.Output()
		if err != nil {
			logrus.Errorf("iptables Output, %v", output)
			continue
		}
	}
	return err
}
